﻿using System;
using System.Collections.Generic;
using Heaven.Texturing;
using Microsoft.Xna.Framework.Graphics;
using Texture = Heaven.Texturing.Texture;
using XNATexture = Microsoft.Xna.Framework.Graphics.Texture;
using CubicTexture = Heaven.Texturing.CubicTexture;
using XNATextureCube = Microsoft.Xna.Framework.Graphics.TextureCube;
using RenderTarget = Heaven.Graphics.RenderTarget;

namespace Heaven.Xna.Graphics
{
    /// <summary>
    /// Caches device representaions of texture objects
    /// </summary>
    public class TextureManager
    {
        #region Fields

        // Cache entity
        Dictionary<long, XNATexture> cache = new Dictionary<long, XNATexture>();
        // Cache for RenderTargets
        Dictionary<long, XnaRenderTarget> renderTargetCache = new Dictionary<long, XnaRenderTarget>();
        // Graphics gevice
        GraphicsDevice device = null;

        // Empty 2D texture
        Texture2D blankPlanarTexture = null;

        #endregion

        #region Properties

        /// <summary>
        /// Count of textures
        /// </summary>
        public int CountOfTextures
        {
            get { return cache.Count; }
        }

        /// <summary>
        /// Count of render targets
        /// </summary>
        public int CountOfRenderTargets
        {
            get { return renderTargetCache.Count; }
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="device">Graphics device</param>
        public TextureManager(GraphicsDevice device) 
        {
            this.device = device;
        }

        /// <summary>
        /// Destructor
        /// </summary>
        ~TextureManager()
        {
            if (blankPlanarTexture != null)
            {
                blankPlanarTexture.Dispose();
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Gets render target
        /// </summary>
        /// <param name="renderTarget">Heaven render target</param>
        /// <returns></returns>
        internal XnaRenderTarget GetRenderTarget(RenderTarget renderTarget)
        {
            XnaRenderTarget xnarendertarget = null;

            lock (renderTargetCache)
            {
                if (!renderTargetCache.TryGetValue(renderTarget.Hash, out xnarendertarget))
                {
                    xnarendertarget = CreateRenderTarget(renderTarget);
                    // Put into cache
                    renderTargetCache.Add(renderTarget.Hash, xnarendertarget);
                    // And when object will be collected by GC
                    renderTarget.GarbageCollected += new EventHandler(OnRenderTargetGarbageCollected);
                    renderTarget.DisposeRequested += new EventHandler(OnRenderTargetDisposeRequested);
                }
                return xnarendertarget;
            }
        }

        /// <summary>
        /// Gets XNA object for this Heaven texture
        /// </summary>
        /// <param name="texture">Texture object</param>
        public XNATexture Get(Texture texture)
        {
            XNATexture xnatexture = null;

            if(texture is RenderTarget)
            {
                lock (renderTargetCache)
                {
                    // If it is a render target
                    xnatexture = GetRenderTarget((RenderTarget)texture).XnaTexture;

                    if (xnatexture == null)
                    {
                        // Render target content is empty
                        return GetBlankPlanarTexture();
                    }
                    return xnatexture;
                }
            }

            lock (cache)
            {
                if (!cache.TryGetValue(texture.Hash, out xnatexture))
                {
                    // Create XNA texture
                    if (texture is PlanarTexture)
                    {
                        xnatexture = CreatePlanarTexture((PlanarTexture)texture);
                    }
                    else if (texture is CubicTexture)
                    {
                        xnatexture = CreateCubicTexture((CubicTexture)texture);
                    }
                    else if (texture is VolumeTexture)
                    {
                        xnatexture = CreateVolumeTexture((VolumeTexture)texture);
                    }

                    // Put into cache
                    cache.Add(texture.Hash, xnatexture);
                    // Subscibe event of changing mesh to remove it from cache
                    texture.Changed += new EventHandler(OnTextureChanged);
                    // And when object will be collected by GC
                    texture.GarbageCollected += new EventHandler(OnTextureGarbageCollected);
                }
            }

            return xnatexture;
        }

        XNATexture GetBlankPlanarTexture()
        {
            if (blankPlanarTexture == null)
            {
                blankPlanarTexture = new Texture2D(device, 2, 2, 1, TextureUsage.None, SurfaceFormat.Color);
                blankPlanarTexture.SetData(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 });
            }
            return blankPlanarTexture;
        }

        #endregion

        #region Event's handlers

        void OnTextureGarbageCollected(object sender, EventArgs e)
        {
            lock (cache)
            {
                // Remove texture from 
                // cache, so it is not valid now
                long hash = ((Texture)sender).Hash;
                cache[hash].Dispose();                
                cache.Remove(hash);
            }
        }

        void OnRenderTargetGarbageCollected(object sender, EventArgs e)
        {
            // Remove render target from 
            // cache, so it is not valid now
            long hash = ((RenderTarget)sender).Hash;
            RemoveRenderTargetFromCache(hash);
        }
        
        void OnRenderTargetDisposeRequested(object sender, EventArgs e)
        {
            // Remove render target from 
            // cache, so it is not valid now
            long hash = ((RenderTarget)sender).Hash;
            RemoveRenderTargetFromCache(hash);
        }

        void RemoveRenderTargetFromCache(long hash)
        {
            lock (renderTargetCache)
            {
                XnaRenderTarget xnaRenderTarget = null;
                if (renderTargetCache.TryGetValue(hash, out xnaRenderTarget))
                {
                    renderTargetCache.Remove(hash);
                    xnaRenderTarget.Dispose();
                }
            }
        }

        void OnTextureChanged(object sender, EventArgs e)
        {
            lock (cache)
            {
                // Remove texture from 
                // cache, so it is not valid now
                long hash = ((Texture)sender).Hash;
                cache[hash].Dispose();
                cache.Remove(hash);
            }
        }

        #endregion

        #region Texture creation methods

        XNATexture CreatePlanarTexture(PlanarTexture texture)
        {
            SurfaceFormat format = SurfaceFormat.Unknown;
            byte[] data = null;
            int bpp = 0;

            // Main layer
            GetTextureLayerData(texture.Layer, out data, out format);
            bpp = GetBytesPerPixel(format);
            Texture2D texture2d = new Texture2D(device, texture.Layer.Width, texture.Layer.Height, texture.Mipmaps.Length + 1, TextureUsage.None, format);
            texture2d.SetData(data);

            // Mipmaps
            for (int i = 0; i < texture.Mipmaps.Length; i++)
            {
                GetTextureLayerData(texture.Mipmaps[i], out data, out format);
                texture2d.SetData(i + 1, null, data, 0, texture.Mipmaps[i].Width * texture.Mipmaps[i].Height * bpp, SetDataOptions.None);
            }
            return texture2d;

        }

        void GetTextureLayerData(Layer layer, out byte[] data, out SurfaceFormat surfaceformat)
        {
            // Select format
            SurfaceFormat format = SelectFormat(layer.Format);

            if (format == SurfaceFormat.Unknown)
            {
                // Convert to default format if 
                // the given one's doesn't supported
                surfaceformat = SurfaceFormat.Color;
                data = LayerFormats.A8R8G8B8.GetData(layer);                
            }
            else
            {
                data = layer.Data;
                surfaceformat = format;
            }
        }

        XNATexture CreateCubicTexture(CubicTexture texture)
        {
            // Get format of the CubeMap
            SurfaceFormat format = SelectFormat(texture.NegativeX.Layer.Format);
            // Default format is SurfaceFormat.Color
            if(format == SurfaceFormat.Unknown) format = SurfaceFormat.Color;

            XNATextureCube cubemap = new XNATextureCube(device, 512, texture.NegativeX.Mipmaps.Length + 1, TextureUsage.None, format);

            SetCubeMapFace(cubemap, CubeMapFace.NegativeX, texture.NegativeX);
            SetCubeMapFace(cubemap, CubeMapFace.NegativeY, texture.NegativeY);
            SetCubeMapFace(cubemap, CubeMapFace.NegativeZ, texture.NegativeZ);
            SetCubeMapFace(cubemap, CubeMapFace.PositiveX, texture.PositiveX);
            SetCubeMapFace(cubemap, CubeMapFace.PositiveY, texture.PositiveY);
            SetCubeMapFace(cubemap, CubeMapFace.PositiveZ, texture.PositiveZ);

            return cubemap;
        }

        XNATexture CreateVolumeTexture(VolumeTexture texture)
        {
            SurfaceFormat format = SurfaceFormat.Unknown;
            byte[] data = null;
            int bpp = 0;

            // Main layer
            GetTextureLayerData(texture.Layer, out data, out format);
            bpp = GetBytesPerPixel(format);
            Texture3D texture3d = new Texture3D(device, texture.Width, texture.Height, texture.Depth, texture.Mipmaps.Length + 1, TextureUsage.None, format);
            texture3d.SetData(data);

            // Mipmaps
            for (int i = 0; i < texture.Mipmaps.Length; i++)
            {
                GetTextureLayerData(texture.Mipmaps[i], out data, out format);
                texture3d.SetData(i + 1,0,0,0,0,0,0, data, 0, texture.Mipmaps[i].Width * texture.Mipmaps[i].Height * texture.Mipmaps[i].Depth * bpp, SetDataOptions.None);
            }
            return texture3d;
        }

        XnaRenderTarget CreateRenderTarget(RenderTarget renderTarget)
        {
            SurfaceFormat format = SelectFormat(renderTarget.Format);

            if (format == SurfaceFormat.Unknown)
            {
                throw new ArgumentException("Render target doesn't support this format " + renderTarget.Format.ToString());
            }

            XnaRenderTarget xnarendertarget = new XnaRenderTarget(device, renderTarget.Width, renderTarget.Height, format);
            renderTarget.DataRequested += delegate 
            {
                Texture2D texture = xnarendertarget.XnaTexture;
                if (texture != null)
                {
                    // To prevent XNA exception retated to potentially 
                    // changing and getting. Atually it doesn't solve 
                    // the problem, so the texture may be setted to 1, 2,.. 
                    // or different register
                    device.Textures[0] = null;
                    texture.GetData(renderTarget.Data);
                }
            };

            return xnarendertarget;
        }

        SurfaceFormat SelectFormat(LayerFormat layerFormat)
        {
            // Select format
            SurfaceFormat format = SurfaceFormat.Unknown;
            if (layerFormat == LayerFormats.A8R8G8B8) format = SurfaceFormat.Color;
            if (layerFormat == LayerFormats.R32F) format = SurfaceFormat.Single;
            if (layerFormat == LayerFormats.A16B16G16R16F) format = SurfaceFormat.HalfVector4;

            return format;
        }

        void SetCubeMapFace(XNATextureCube cubemap, CubeMapFace face, PlanarTexture texture)
        {
            SurfaceFormat format = SurfaceFormat.Unknown;
            byte[] data = null;
            int bpp = 0;

            // Main layer
            GetTextureLayerData(texture.Layer, out data, out format);
            bpp = GetBytesPerPixel(format);
            cubemap.SetData(face, 0, null, data, 0, texture.Layer.Width * texture.Layer.Height * bpp, SetDataOptions.None);
                        
            for (int i = 0; i < texture.Mipmaps.Length; i++)
            {
                GetTextureLayerData(texture.Mipmaps[i], out data, out format);
                cubemap.SetData(face, i + 1, null, data, 0, texture.Mipmaps[i].Width * texture.Mipmaps[i].Height * bpp, SetDataOptions.None);
            }
        }

        int GetBytesPerPixel(SurfaceFormat format)
        {
            switch (format)
            {
                case SurfaceFormat.Color: return 4;
            }

            throw new ArgumentException("There is no support getting BPP for " + format.ToString() + " in TextureManager");
        }

        #endregion

        #region Routine

        

        #endregion
    }
}
